#include "TaggedString.hpp"
#include "UnitTest.hpp"
#include <string.h>

TaggedString::TaggedString()
{
	tags = NULL;
	unfinished = notUnfinished;
}

TaggedString::TaggedString(const char *str)
	:std::string(str)
{
	buildTags();
}

TaggedString::TaggedString(const std::string &str)
	:std::string(str)
{
	buildTags();
}

TaggedString::TaggedString(const TaggedString &str)
	:std::string(str)
{
	unsigned len = length();
	if(len) {
		tags = new unsigned char[len];
		memcpy(tags, str.tags, len);
	} else {
		tags = NULL;
	}
	
	unfinished = str.unfinished;
}

TaggedString::~TaggedString()
{
	if(tags)
		delete[] tags;
}

void TaggedString::buildTags()
{
	unfinished = notUnfinished;
	
	unsigned len = length();
	if(!len) {
		tags = NULL;
		return;
	}
	tags = new unsigned char[len];
	
	for(size_t ii=0; ii<len; ii++)
		tags[ii] = 0;
	
	int quoteState = 0;
	int prevQuoteState = 0;
	
	for(size_t ii=0; ii<len; ii++)
	{
		char ch = (*this)[ii];
		
		tags[ii] |= quoteState;
		
		if(!(tags[ii] & tagEscaped))
		{
			if(ch=='\\')
			{
				// Where we see an unescaped backslash, mark the next character as
				// escaped. Since we're reproducing shell behavior, escaped escape
				// characters do not themselves escape characters. (Single quotes
				// block use of escape sequences, even escapes of single quotes.)
				if(quoteState != tagSingleQuoted)
				{
					if(ii+1 < len) {
						tags[ii+1] |= tagEscaped;
						
						// Now mark whether this backslash gets deleted in
						// expansion. Outside quotes or inside backquotes, it
						// always does. Inside double quotes, it only counts if
						// the next char is a quote (of any type) or a $.
						if(quoteState == tagDoubleQuoted) {
							char nextch = (*this)[ii+1];
							if(strchr("`'\"$", nextch))
								tags[ii] |= tagEscape;
						} else {
							tags[ii] |= tagEscape;
						}
					} else {
						tags[ii] |= tagEscape;
						unfinished = danglingBackslash;
					}
				}
			}
			else if(ch=='\"')
			{
				if(quoteState == 0) {
					quoteState = tagDoubleQuoted;
					tags[ii] |= tagQuote;
				} else if(quoteState==tagDoubleQuoted) {
					quoteState = 0;
					tags[ii] |= tagQuote;
					tags[ii] &= ~tagDoubleQuoted;
				}
			}
			else if(ch=='\'')
			{
				if(quoteState == 0) {
					quoteState = tagSingleQuoted;
					tags[ii] |= tagQuote;
				} else if(quoteState==tagSingleQuoted) {
					quoteState = 0;
					tags[ii] |= tagQuote;
					tags[ii] &= ~tagSingleQuoted;
				}
			}
			else if(ch=='`')
			{
				if(quoteState==0 || quoteState==tagDoubleQuoted)
				{
					prevQuoteState = quoteState;
					quoteState = tagBackQuoted;
					tags[ii] |= tagQuote;
				}
				else if(quoteState==tagBackQuoted)
				{
					quoteState = prevQuoteState;
					prevQuoteState = 0;
					tags[ii] &= ~tagBackQuoted;
					tags[ii] |= tagQuote | prevQuoteState;
				}
			}
		}
	}
	
	if(quoteState==tagDoubleQuoted)
		unfinished = danglingDoubleQuote;
	else if(quoteState==tagSingleQuoted)
		unfinished = danglingSingleQuote;
	else if(quoteState==tagBackQuoted)
		unfinished = danglingBackQuote;
}

bool TaggedString::isUnfinished() const
{
	return unfinished!=notUnfinished;
}

bool TaggedString::hasUnfinishedEscapeSeqence() const
{
	return unfinished==danglingBackslash;
}

bool TaggedString::hasUnmatchedQuote() const
{
	return unfinished==danglingSingleQuote || unfinished==danglingDoubleQuote || unfinished==danglingBackQuote;
}

char TaggedString::getUnmatchedQuote() const
{
	if(unfinished==danglingSingleQuote)
		return '\'';
	else if(unfinished==danglingDoubleQuote)
		return '\"';
	else if(unfinished==danglingBackQuote)
		return '`';
	else
		return 0;
}

int TaggedString::findClosingQuote(size_t index)
{
	if(index>=length() || !isQuote(index))
		return index;
	
	char quote = (*this)[index];
	size_t pos;
	
	for(pos=index+1; pos<length(); pos++)
	{
		if(isQuote(pos) && (*this)[pos]==quote)
			break;
	}
	
	return pos;
}

TEST_SET(TaggedString)
{
	//                 01 23 4
	TaggedString str1("a\"b\"c"); // a"b"c
	testAssert(!str1.isUnfinished());
	testAssert(!str1.isQuoted(0));
	testAssert(!str1.isQuoted(1));
	testAssert(str1.isQuote(1));
	testAssert(str1.isQuoted(2));
	testAssert(str1.isDoubleQuoted(2));
	testAssert(!str1.isQuoted(3));
	testAssert(str1.isQuote(3));
	testAssert(!str1.isQuoted(4));
	testAssert(str1.findClosingQuote(1) == 3);
	
	//                 01 2 34 5
	TaggedString str2("a\\\"b\"c"); // a\"b"c
	testAssert(str2.isUnfinished());
	testAssert(str2.hasUnmatchedQuote());
	testAssert(str2.getUnmatchedQuote()=='\"');
	testAssert(!str2.isEscaped(1));
	testAssert(!str2.isQuote(2));
	testAssert(str2.isEscaped(2));
	testAssert(str2.isQuote(4));
	testAssert(str2.isDoubleQuoted(5));
	testAssert(str2.findClosingQuote(4) == 6);
	
	TaggedString str3(""); // empty string
	testAssert(!str3.isUnfinished());
	
	//                 01 23456 7
	TaggedString str4("x\"it's\"x"); // x"it's"x
	testAssert(!str4.isUnfinished());
	testAssert(!str4.isQuote(4));
	testAssert(str4.isDoubleQuoted(4));
	testAssert(!str4.isSingleQuoted(5));
	testAssert(str4.findClosingQuote(1) == 6);
	
	//                 01234
	TaggedString str5("'a b'"); // 'a b'
	testAssert(!str5.isUnfinished());
	testAssert(str5.isQuote(0));
	testAssert(str5.isSingleQuoted(1));
	testAssert(str5.isQuote(4));
	
	//                 0123
	TaggedString str6("`a b");
	testAssert(str6.isUnfinished());
	testAssert(str6.isQuote(0));
	testAssert(str6.isBackQuoted(1));
	testAssert(!str6.isQuoted(0));
}


